値の分布に偏りがあるフィールドに対してフィールドインデックスを設定した場合の動作を検証してみた
リテールアプリ共創部@大阪の岩田です。
2024/11/21付けのアップデートによってCW Logs Insightsのクエリでフィールドインデックスが利用可能になりました。この機能を活用することで、クエリによるスキャン量低減とコストの最適化が実現できます。
フィールドインデックスを設定する対象としてはカーディナリティの高いフィールドが推奨されており、公式ドキュメントには以下のように記載されています。
Fields that have high cardinality of values are also good candidates for field indexes because a query using these field indexes will complete faster because it limits the log events that are being matched to the target value.
フィールドインデックスの内部構造の詳細は明かされていませんが、この辺の考え方は一般的なRDBにおけるインデックスと同じ考え方になりそうですね。ここで気になったのが、カーディナリティの低いフィールドに対してフィールドインデックスを設定した場合の動作がどうなるか?という点です。
RDBでもカーディナリティの低い列に対するインデックスは推奨されませんが、これは値の分布が均一であるという前提にもとづいています。例えば'0'と'1'しか取り得ないisRare
というカラムがあったとして、'0'と'1'が99.999:0.001の割合で分布しているとします。この例ではSELECT ...略 WHERE isRare = '0'
というクエリはインデックスを利用すると逆に効率が悪くなるため実行計画としてはスキャンが採用されるはずです。対してSELECT ...略 WHERE isRare = '1'
というクエリはインデックスを利用することで効率的にレコードが取得できるため、実行計画はインデックスを用いたものが採用されるはずです。
このように値の分布が偏ることが明白なケースではカーディナリティの低いカラムであってもインデックスを貼ることがあります。
※DBMS側が対応していれば部分インデックスを使うとより効率的です。
上記のようにカーディナリティは低いものの値の分布に偏りがあるフィールドに対してフィールドインデックスを設定した場合にCW Logs Insightsのクエリがフィールドインデックスを利用してくれるのか気になったので実際に検証してみました。
以後はすべてフィールドインデックスがBツリーもしくはB+ツリー構造という仮定に基づいて考察していきます。
やってみる
それでは実際に検証してみます。
ロググループの作成
まず以下のCFnテンプレートでCW Logsのロググループとログストリームを作成します。
AWSTemplateFormatVersion: 2010-09-09
Resources:
HasRareAttributeLogGroup:
Type: 'AWS::Logs::LogGroup'
DeletionPolicy: Delete
Properties:
RetentionInDays: 7
LogGroupName: HasRareAttribute
FieldIndexPolicies:
- Fields:
- isRare
HasRareAttributeLogStream:
Type: AWS::Logs::LogStream
Properties:
LogGroupName: !Ref HasRareAttributeLogGroup
LogStreamName: default
上記テンプレートを使ってスタックをデプロイします。
aws cloudformation create-stack --stack-name fld-index-test --template-body file://template.yaml
デプロイされたロググループの定義は以下の通りです。
フィールドインデックスポリシーでisRare
というフィールドに対してフィールドインデックスが定義されています。
ログの書き込み
続いて以下のPythonスクリプトで上記ロググループに対してログを書き込みます。
import json
import time
import boto3
client = boto3.client('logs')
for i in range(10):
log_events = []
for j in range(10000):
if j == 0:
is_rare = "1"
else:
is_rare = "0"
log_event = {
'timestamp': int(time.time()) * 1000,
'message': json.dumps({'msg': f'{i}-{j}', 'isRare': is_rare})
}
log_events.append(log_event)
res = client.put_log_events(logGroupName='HasRareAttribute',logStreamName='default',logEvents=log_events)
print(res)
1万件に1件の割合でisRare
というフィールドが1
になるようにログを生成し、合計10万件のログを書き込んでいます。スクリプトの実行完了後に意図通りログが書き込めているか以下のクエリで確認してみましょう。
stats count(isRare) by isRare
結果は以下の通りでした。
isRare
が1
のログが10件、isRare
が0
のログが99,990件書き込まれています。
分布に偏りのあるフィールドを指定したクエリを実行
ログの準備ができたのでフィルター条件にisRare
を指定してクエリを実行してみます。まずはisRare
が0
のログをクエリしてみます。ほとんどのログはisRare
の値が0
なのでインデックスを利用すると逆に効率が悪くなるはずです。
filter isRare = '0'
結果は以下の通りでした。
99,990 の 10000 の一致したレコードの表示
100,000 レコード (6.0 MB) が 2.6s @ 38,491 records/s (2.3 MB/s) でスキャンされました
と出力されており、フィールドインデックスが利用されていないことが分かります。
続いてisRare
が1
のログをクエリしてみます。isRare
が1
のログはは10件だけなのでフィールドインデックスを利用すると効率よくクエリできるはずです。
filter isRare = '1'
結果は以下の通りでした。
10 の 10 の一致したレコードの表示
100,000 レコード (6.0 MB) が 1.2s @ 84,674 records/s (5.1 MB/s) でスキャンされました
と出力されており、こちらのパターンでもフィールドインデックスは利用されませんでした。
まとめ
この後ログの量を増やしたり、分布の割合を微調整したり、色々試したのですが今回の検証の範囲ではisRare
に設定したフィールドインデックスが利用されることはありませんでした。統計情報のような概念が存在するのか?とか、仮に存在するのであればサンプリングレートはどの程度なのか?とか色々と妄想は膨らみますが、RDBにおけるBツリー/B+ツリーインデックスと同じような感覚でフィールドインデックスポリシーを設計するのは避けたほうが良さそうです。意図通りフィールドインデックスが利用されているか実際にクエリしながらテストするのが大事そうですね。